﻿using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using Black.SurfaceToolkit.Tools;
using Microsoft.Surface.Presentation.Controls;

namespace Black.SurfaceToolkit.Controls
{
    /// <summary>
    /// Représente une liste en mode carousel.
    /// </summary>
    /// <remarks>
    /// <para>Ce contrôle affiche ses éléments sous forme de caroussel. Basé sur <see cref="T:Black.SurfaceToolkit.Controls.StringPathPanel" />, il dispose les éléments selon un chemin définit.</para>
    /// <para><see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.ZIndexMode" /> indique au composant la manière dont doivent être calculés les <see cref="P:System.Windows.Controls.Panel.ZIndex" /> des éléments enfants.</para>
    /// <para><see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.RelativeIndex" /> indique la position de chaque élément relativement à l'élément séléctionné.</para>
    /// <para>Ce contrôle définit les propriétés des conteneurs de façon à ce qu'aucun changement (ajout, suppression, déplacement) ne provoque de rupture visuelle. Il permet également à plusieurs manipulations de se produire simultanément.</para>
    /// <para><div class="caption" style="font-weight : bold">CoverFlow</div></para>
    /// <para><img src="../Images/CoverFlow.Normal.png" /></para>
    /// </remarks>
    [Browsable(true), Description("Liste en mode caroussel")]
    public class CoverFlow : SurfaceListBox
    {
        #region Variables de classe

        private static DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(StringPathPanel.PositionProperty, typeof(StringPathPanel));

        #endregion

        #region Propriétés de dépendance

        /// <summary>
        /// Clé de la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.RelativeIndex" />.
        /// </summary>
        private static readonly DependencyPropertyKey RelativeIndexPropertyKey = DependencyProperty.RegisterAttachedReadOnly("RelativeIndex", typeof(int), typeof(CoverFlow), new FrameworkPropertyMetadata(0));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.IsZIndexInverted" />.
        /// </summary>
        public static readonly DependencyProperty IsZIndexInvertedProperty = DependencyProperty.Register("IsZIndexInverted", typeof(bool), typeof(CoverFlow), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox, new PropertyChangedCallback(CoverFlow.OnIsZIndexInvertedChanged)));
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.ZIndexMode" />.
        /// </summary>
        public static readonly DependencyProperty ZIndexModeProperty = DependencyProperty.Register("ZIndexMode", typeof(CoverFlowZIndexMode), typeof(CoverFlow), new FrameworkPropertyMetadata(CoverFlowZIndexMode.ItemsPosition, new PropertyChangedCallback(CoverFlow.OnZIndexModeChanged)), new ValidateValueCallback(CoverFlow.IsValidZIndexMode));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.RelativeIndex" />.
        /// </summary>
        public static readonly DependencyProperty RelativeIndexProperty = CoverFlow.RelativeIndexPropertyKey.DependencyProperty;

        #endregion

        #region Accesseurs
        
        /// <summary>
        /// Obtient ou définit une valeur indiquant si <see cref="P:System.Windows.Controls.Panel.ZIndex" /> est inversé.
        /// </summary>
        [Browsable(true), DefaultValue(false), Description("Valeur indiquant si le ZIndex est inversé"), Category("CoverFlow")]
        public bool IsZIndexInverted
        {
            get
            {
                return (bool)this.GetValue(CoverFlow.IsZIndexInvertedProperty);
            }
            set
            {
                this.SetValue(CoverFlow.IsZIndexInvertedProperty, BooleanBoxes.Box(value));
            }
        }

        /// <summary>
        /// Obtient ou définit une valeur indiquant comment calculer le <see cref="P:System.Windows.Controls.Panel.ZIndex" />.
        /// </summary>
        [Browsable(true), DefaultValue(false), Description("Valeur indiquant le mode de calcul des ZIndex"), Category("CoverFlow")]
        public CoverFlowZIndexMode ZIndexMode
        {
            get
            {
                return (CoverFlowZIndexMode)this.GetValue(CoverFlow.ZIndexModeProperty);
            }
            set
            {
                this.SetValue(CoverFlow.ZIndexModeProperty, value);
            }
        }

        #endregion

        #region Constructeurs

        /// <summary>
        /// Initialise la classe <see cref="T:Black.SurfaceToolkit.Controls.CoverFlow" />.
        /// </summary>
        static CoverFlow()
        {
            FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(typeof(CoverFlow), new FrameworkPropertyMetadata(typeof(CoverFlow)));
            Selector.SelectedIndexProperty.OverrideMetadata(typeof(CoverFlow), new FrameworkPropertyMetadata(-1, null, new CoerceValueCallback(CoverFlow.CoerceSelectedIndex)));
        }

        /// <summary>
        /// Initialise une nouvelle instance de <see cref="T:Black.SurfaceToolkit.Controls.CoverFlow" />.
        /// </summary>
        public CoverFlow()
            : base()
        {
            this.ItemContainerGenerator.StatusChanged += new EventHandler((sender, e) => this.ItemContainerGeneratorStatusChanged());
        }

        #endregion

        #region Fonctions publiques

        /// <summary>
        /// Obtient la position relativement à l'élément séléctionné de l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut la position relativement à l'élément séléctionné.</param>
        /// <returns>Position relativement à l'élément séléctionné de <paramref name="p_oElement" />.</returns>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="p_oElement" /> est <see langword="null" />.</exception>
        [AttachedPropertyBrowsableForChildren]
        public static int GetRelativeIndex(UIElement p_oElement)
        {
            if (p_oElement == null)
                throw new ArgumentNullException("p_oElement");

            return (int)p_oElement.GetValue(CoverFlow.RelativeIndexProperty);
        }

        #endregion

        #region Fonctions protégées

        /// <summary>
        /// Nettoie le conteneur de l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Conteneur à nettoyer.</param>
        /// <param name="p_oItem">Elément associé.</param>
        protected override void ClearContainerForItemOverride(DependencyObject p_oElement, object p_oItem)
        {
            base.ClearContainerForItemOverride(p_oElement, p_oItem);

            CoverFlow.descriptor.RemoveValueChanged(p_oElement, new EventHandler(this.ListBoxItemPositionChanged));
        }

        /// <summary>
        /// Appelée lorsque la propriété <see cref="P:System.Windows.Controls.Primitives.Selector.SelectedIndex" /> est réévaluée.
        /// </summary>
        /// <param name="p_iValue">Valeur à adapter.</param>
        /// <returns><paramref name="p_iValue" /> adapté.</returns>
        protected virtual int CoerceSelectedIndex(int p_iValue)
        {
            if (this.Items.Count > 0 && p_iValue < 0)
                return (0);
            
            return (p_iValue);
        }

        /// <summary>
        /// Calcule les indices relativement à l'élément séléctionné.
        /// </summary>
        protected virtual void ComputeRelativeIndexes()
        {
            if (this.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
                return;

            int iSelectedIndex = this.SelectedIndex;
            ItemCollection oItems = this.Items;

            if (iSelectedIndex > -1 && iSelectedIndex < oItems.Count)
            {
                for (int i = 0; i <= iSelectedIndex; i++)
                {
                    object oItem = oItems[i];

                    UIElement oContainer = this.ContainerAtAndOf<UIElement>(i, oItem);

                    StringPathPanel.SetPrecedingMargin(oContainer, StringPathPanel.GetPrecedingMargin(oContainer));
                    StringPathPanel.SetFollowingMargin(oContainer, StringPathPanel.GetFollowingMargin(oContainer));
                }

                int iCountItem = oItems.Count;

                int iSelectedZIndex = Math.Max(iSelectedIndex, iCountItem - (iSelectedIndex + 1));

                for (int i = 0; i <= iSelectedIndex; i++)
                {
                    object oItem = oItems[i];

                    UIElement oContainer = this.ContainerAtAndOf<UIElement>(i, oItem);

                    if (oContainer != null)
                    {
                        int iDistance = iSelectedIndex - i;
                        CoverFlow.SetRelativeIndex(oContainer, iDistance);
                    }
                }

                for (int i = iSelectedIndex + 1; i < iCountItem; i++)
                {
                    object oItem = oItems[i];

                    UIElement oContainer = this.ContainerAtAndOf<UIElement>(i, oItem); ;

                    if (oContainer != null)
                    {
                        int iDistance = i - iSelectedIndex;
                        CoverFlow.SetRelativeIndex(oContainer, iDistance);
                    }
                }
            }
        }

        /// <summary>
        /// Calcule les indices.
        /// </summary>
        protected virtual void ComputeZIndexes()
        {
            if (this.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
                return;

            bool bIsZIndexInverted = this.IsZIndexInverted;

            switch (this.ZIndexMode)
            {
                case CoverFlowZIndexMode.Custom:
                    return;
                case CoverFlowZIndexMode.ItemsOrder:
                    {
                        int iSelectedIndex = this.SelectedIndex;
                        ItemCollection oItems = this.Items;

                        if (iSelectedIndex > -1 && iSelectedIndex < oItems.Count)
                        {
                            int iCountItem = oItems.Count;

                            int iSelectedZIndex = Math.Max(iSelectedIndex, iCountItem - (iSelectedIndex + 1));

                            for (int i = 0; i <= iSelectedIndex; i++)
                            {
                                object oItem = oItems[i];

                                UIElement oContainer = this.ContainerAtAndOf<UIElement>(i, oItem);

                                if (oContainer != null)
                                {
                                    int iDistance = iSelectedIndex - i;
                                    Panel.SetZIndex(oContainer, bIsZIndexInverted ? iDistance : iSelectedZIndex - iDistance);
                                }
                            }

                            for (int i = iSelectedIndex + 1; i < iCountItem; i++)
                            {
                                object oItem = oItems[i];

                                UIElement oContainer = this.ContainerAtAndOf<UIElement>(i, oItem); ;

                                if (oContainer != null)
                                {
                                    int iDistance = i - iSelectedIndex;
                                    Panel.SetZIndex(oContainer, bIsZIndexInverted ? iDistance : iSelectedIndex - iDistance);
                                }
                            }
                        }
                    }
                    break;
                case CoverFlowZIndexMode.ItemsPosition:
                    {
                        ItemCollection oItems = this.Items;
                        int iCountItems = oItems.Count;

                        for (int i = 0; i < iCountItems; i++)
                        {
                            object oItem = oItems[i];

                            UIElement oContainer = this.ContainerAtAndOf<UIElement>(i, oItem);

                            if (oContainer != null)
                            {
                                int iPosition = StringPathPanel.GetPosition(oContainer);

                                if (iPosition < 0)
                                {
                                    iPosition = -iPosition;
                                }

                                Panel.SetZIndex(oContainer, bIsZIndexInverted ? iPosition : int.MaxValue - iPosition);
                            }
                        }
                    }
                    break;
            }
        }

        /// <summary>
        /// Obtient un nouveau conteneur pour les éléments.
        /// </summary>
        /// <returns>Nouvelle instance de <see cref="T:Black.SurfaceToolkit.Controls.CoverFlowItem" />.</returns>
        protected override DependencyObject GetContainerForItemOverride()
        {
            return (new CoverFlowItem());
        }

        /// <summary>
        /// Obtient une valeur indiquant si l'élément spécifié est son propre conteneur.
        /// </summary>
        /// <param name="p_oItem">Objet à tester.</param>
        /// <returns><see langword="true" /> si <paramref name="p_oItem" /> est son propre conteneur, <see langword="false" /> sinon.</returns>
        protected override bool IsItemItsOwnContainerOverride(object p_oItem)
        {
            return (p_oItem is CoverFlowItem);
        }

        /// <summary>
        /// Appelée lorsque l'état du générateur de conteneurs change.
        /// </summary>
        protected virtual void ItemContainerGeneratorStatusChanged()
        {
            if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
            {
                this.ComputeZIndexes();
                this.ComputeRelativeIndexes();
            }
        }

        /// <summary>
        /// Appelée lorsque la propriété <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.IsZIndexInverted" /> est modifiée.
        /// </summary>
        /// <param name="p_oArgs">Données de l'événement.</param>
        protected virtual void OnIsZIndexInvertedChanged(DependencyPropertyChangedEventArgs p_oArgs)
        {
            ComputeZIndexes();
        }

        /// <summary>
        /// Appelée lorsque la collection <see cref="P:System.Windows.Controls.ItemsControl.Items" /> est modifiée.
        /// </summary>
        /// <param name="p_oArgs">Données de l'événement.</param>
        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs p_oArgs)
        {
            base.OnItemsChanged(p_oArgs);

            this.InvalidateProperty(Selector.SelectedIndexProperty);

            this.ComputeZIndexes();
            this.ComputeRelativeIndexes();
        }
        
        /// <summary>
        /// Appelée lorsque la séléction change.
        /// </summary>
        /// <param name="p_oArgs">Donnée de l'événement.</param>
        protected override void OnSelectionChanged(SelectionChangedEventArgs p_oArgs)
        {
            base.OnSelectionChanged(p_oArgs);

            this.ComputeZIndexes();
            this.ComputeRelativeIndexes();
        }

        /// <summary>
        /// Appelée lorsque la propriété <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.ZIndexMode" /> est modifiée.
        /// </summary>
        /// <param name="p_oArgs">Données de l'événement.</param>
        protected virtual void OnZIndexModeChanged(DependencyPropertyChangedEventArgs p_oArgs)
        {
            this.ComputeZIndexes();
        }

        /// <summary>
        /// Prépare un conteneur pour recevoir un élément.
        /// </summary>
        /// <param name="p_oElement">Conteneur à préparer avec <paramref name="p_oItem" />.</param>
        /// <param name="p_oItem">Elément reçu par <paramref name="p_oElement" />.</param>
        protected override void PrepareContainerForItemOverride(DependencyObject p_oElement, object p_oItem)
        {
            base.PrepareContainerForItemOverride(p_oElement, p_oItem);

            CoverFlow.descriptor.AddValueChanged(p_oElement, new EventHandler(this.ListBoxItemPositionChanged));
        }

        #endregion

        #region Fonctions privées

        /// <summary>
        /// Obtient le conteneur de l'objet spécifié
        /// </summary>
        /// <typeparam name="T">Type du conteneur</typeparam>
        /// <param name="p_iItemIndex">Position de l'élément</param>
        /// <param name="p_oItem">Objet dont on veut le conteneur</param>
        /// <returns>Conteneur d'<paramref name="p_oItem" /></returns>
        private T ContainerAtAndOf<T>(int p_iItemIndex, object p_oItem)
            where T : class
        {
            if (this.IsItemItsOwnContainerOverride(p_oItem))
            {
                return (p_oItem as T);
            }
            else
            {
                return (this.ItemContainerGenerator.ContainerFromIndex(p_iItemIndex) as T);
            }
        }

        /// <summary>
        /// Appelée lorsque la valeur de la propriété <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.Position" /> est modifiée.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement</param>
        /// <param name="p_oArgs">Données de l'événement</param>
        private void ListBoxItemPositionChanged(object p_oSender, EventArgs p_oArgs)
        {
            this.ComputeZIndexes();
        }

        #endregion

        #region Fonctions statiques privées

        /// <summary>
        /// Appelée lorsque la propriété <see cref="P:System.Windows.Controls.Primitives.Selector.SelectedIndex" /> est réévaluée.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement.</param>
        /// <param name="p_oValue">Valeur à adapter.</param>
        /// <returns><paramref name="p_oValue" /> adapté.</returns>
        private static object CoerceSelectedIndex(DependencyObject p_oSender, object p_oValue)
        {
            return ((CoverFlow)p_oSender).CoerceSelectedIndex((int)p_oValue);
        }

        /// <summary>
        /// Valide un mode de calcul de <see cref="P:System.Windows.Controls.Panel.ZIndex" />.
        /// </summary>
        /// <param name="p_oValue">Mode de calcul de <see cref="P:System.Windows.Controls.Panel.ZIndex" /></param>
        /// <returns>Valeur indiquant si un mode de calcul de <see cref="P:System.Windows.Controls.Panel.ZIndex" /> est valide.</returns>
        private static bool IsValidZIndexMode(object p_oValue)
        {
            CoverFlowZIndexMode eMode = (CoverFlowZIndexMode)p_oValue;

            return eMode == CoverFlowZIndexMode.Custom
                || eMode == CoverFlowZIndexMode.ItemsOrder
                || eMode == CoverFlowZIndexMode.ItemsPosition;
        }

        /// <summary>
        /// Appelée lorsque la propriété <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.IsZIndexInverted" /> est modifiée.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement.</param>
        /// <param name="p_oArgs">Données de l'événement.</param>
        private static void OnIsZIndexInvertedChanged(DependencyObject p_oSender, DependencyPropertyChangedEventArgs p_oArgs)
        {
            ((CoverFlow)p_oSender).OnIsZIndexInvertedChanged(p_oArgs);
        }

        /// <summary>
        /// Appelée lorsque la propriété <see cref="P:Black.SurfaceToolkit.Controls.CoverFlow.CoverFlowZIndexMode" /> est modifiée.
        /// </summary>
        /// <param name="p_oSender">Source de l'événement.</param>
        /// <param name="p_oArgs">Données de l'événement.</param>
        private static void OnZIndexModeChanged(DependencyObject p_oSender, DependencyPropertyChangedEventArgs p_oArgs)
        {
            ((CoverFlow)p_oSender).OnZIndexModeChanged(p_oArgs);
        }

        /// <summary>
        /// Définit la position relativement à l'élément séléctionné de l'élément spécifié.
        /// </summary>
        /// <param name="p_oElement">Elément dont on veut définir la position relativement à l'élément séléctionné.</param>
        /// <param name="p_iValue">Position relativement à l'élément séléctionné de <paramref name="p_oElement" />.</param>
        /// <exception cref="T:System.ArgumentNullException"><paramref name="p_oElement" /> est <see langword="null" />.</exception>
        [AttachedPropertyBrowsableForChildren]
        private static void SetRelativeIndex(UIElement p_oElement, int p_iValue)
        {
            if (p_oElement == null)
                throw new ArgumentNullException("p_oElement");

            p_oElement.SetValue(CoverFlow.RelativeIndexPropertyKey, p_iValue);
        }

        #endregion
    }
}
